#!/usr/bin/env python3

###############################################################################
# Name:        xml2dict.py
# Purpose:     function for converting xml to python3::dict
# Author:      Bright Li
# Modified by:
# Created:     2018-08-16
# Version:     [1.0.3]
# RCS-ID:      $$
# Copyright:   (c) Bright Li
# Licence:
###############################################################################

import os.path
import logging
import re
import json
try:
    import xml.etree.cElementTree as ET
except ImportError:
    import xml.etree.ElementTree as ET

# logging.basicConfig(level=logging.ERROR,
#     format="[Xml2Dict] %(levelname)s --> %(message)s", filename="xml2dict.log")

"""
[方案1]
对于xml的解析，tag代表该层级对象的类型，反映为key-value中的key；
attrib解析为value(dict)内部的键值对，其名称加前缀 @
text解析为value(dict)内部的键值对，其名称为 #text

[方案2]
attrib解析为value["attr"]
子标签解析为value["sub-node"]

[方案3]
将Node作为对象，其text、attr、sub-node作为列表项。数据结构如下：
dict_node = \
{'keyboard': (None,
              {'default_page': 'MP0',
               'h': '768',
               'qssfile': './default.qss',
               'w': '342',
               'x': '40',
               'y': '60'},
              {'page': [(None,
                         {'id': 'MP0', 'svgfile': './res/structure.svg'},
                         {'btn': [(None,
                                   {'id': 'FA0101',
                                    'sticky': 'false',
                                    'text': 'FirstKey'},
                                   {'key_char': [(None,
                                                  {'group': '', 'key': 't'},
                                                  None)]}),
                                  (None,
                                   {'id': 'FA0102',
                                    'sticky': 'false',
                                    'text': '第二个键'},
                                   {'shell': [(None,
                                               {'cmd': 'zenity --info --text '
                                                       "'Hello, world.'"},
                                               None)]}),
                                  (None,
                                   {'id': 'FA0103', 'sticky': 'false'},
                                   {'key_mode': [(None,
                                                  {'group': '',
                                                   'key': 'Shift',
                                                   'sticky': 'true'},
                                                  None)]})]})]})}
"""

class XmlNode:
    def __init__(self):
        self.tag = None
        self.text = None
        self.attr = None  # a dict of attribute
        self.subnode = None  # a dict of sub-node

    def __str__(self):
        return str(self.dump_to_dict())

    def load_from_dict(self, dict_data):
        """ depend on the data structe of dict_data """
        if len(dict_data) != 1:
            raise Exception("Not Well-formed Data [in dict]")
        for key, value in dict_data.items():
            self.tag = key
            self.text = value[0]  # if value[0] else None --> Maybe the value is 'False'
            self.attr = value[1]
            self.subnode = value[2]

    def dump_to_dict(self):
        """ return to dict """
        if not self.tag:
            raise Exception("Uninitialized Object")
        dict_dump = { self.tag: (
                            self.text,  # the tag's text
                            self.attr,  # the tag's attribute dict
                            self.subnode  # the tag's sub-node dict
                        )
                    }
        return dict_dump

    def dump_to_json(self):
        """ return a string of json """
        dict_dump = self.dump_to_dict()
        return json.dumps(dict_dump)

    # def _dump_to_list_without_mytag(self):
    #     list_dump = [
    #                     self.text,  # the tag's text
    #                     self.attr,  # the tag's attribute dict
    #                     self.subnode  # the tag's sub-node dict
    #                 ]
    #     return list_dump

    # def load_from_string(self, str_dict_data):
    #     data = eval(str_dict_data)
    #     self.load_from_dict(data)

    def load_from_file(self, file_path, ftype="xml"):
        if ftype == "xml":
            xml_root = ET.parse(file_path)
            elem_root = xml_root.getroot()
            self._parse_xml_node(elem_root)
        elif ftype == "json":
            raise Exception("Not Finished with json")
        else:
            raise Exception("Unknown type of the file_path")

    def load_from_xml(self, str_xml):
        elem_root = ET.fromstring(str_xml)
        self._parse_xml_node(elem_root)

    def _parse_xml_node(self, etree_xml_elem):
        """ return dict of the current etree_xml_elem """
        self.tag = etree_xml_elem.tag
        # logging.debug("-->>" + etree_xml_elem.tag + "-->>" + etree_xml_elem.tail + "<<--!")
        # logging.debug("-->>" + etree_xml_elem.tag + "-->>" + "Space" if etree_xml_elem.text.isspace() else etree_xml_elem.text)

        if etree_xml_elem.text and not re.match(r'\n?\s*$', etree_xml_elem.text):
            self.text = etree_xml_elem.text

        if etree_xml_elem.attrib:
            self.attr = {}
            for key, value in etree_xml_elem.attrib.items():
                # dict_attr["@"+key] = value
                self.attr[key] = value

        dict_subnode = {}
        for child in etree_xml_elem:
            # 此处递归创建新的XmlNode对象，但由于数据结构是递归引用，因而并不会耗费多余的资源
            xml_node = XmlNode()
            this_subnode = xml_node._parse_xml_node(child)
            subnode_value = this_subnode[child.tag]  # this is a tuple

            if child.tag not in dict_subnode:
                dict_subnode[child.tag] = [subnode_value]
            # elif isinstance(dict_subnode[child.tag], tuple):  # 已存在同名子节点，但子节点类型为()，则改为[]
            #     dict_subnode[child.tag] = [dict_subnode[child.tag], subnode_value]
            else:
                dict_subnode[child.tag].append(subnode_value)

        if dict_subnode:
            self.subnode = dict_subnode

        return self.dump_to_dict()

    def dump_to_xml(self):
        """ return a string of xml """
        str_header = '<?xml version="1.0"?>\n'
        xml_elem_root = self._build_xml_docs(None, self.dump_to_dict())
        return str_header + ET.tostring(xml_elem_root, encoding="unicode")

    def _build_xml_docs(self, parent_elem, dict_data):
        if not dict_data:
            raise Exception("Null data to dump")

        xml_elem = None
        for tag, element in dict_data.items():
            # 处理节点属性
            if parent_elem is None:
                xml_elem = ET.Element(tag, element[1] if element[1] else {})
            else:
                xml_elem = ET.SubElement(parent_elem, tag, element[1] if element[1] else {})

            # 无原则添加换行
            xml_elem.tail = "\n"

            # 无text则换行
            if element[0]:
                xml_elem.text = element[0]
            elif element[2]:
                xml_elem.text = "\n"

            if element[2]:
                for sub_tag, sub_elem in element[2].items():
                    # if isinstance(sub_elem, tuple):
                    #     self._build_xml_docs(xml_elem, {sub_tag: sub_elem})
                    # if isinstance(sub_elem, list):  # 处理同名标签
                        for elem_item in sub_elem:
                            self._build_xml_docs(xml_elem, {sub_tag: elem_item})
        return xml_elem

    def save_to_file(self, file_path, file_type="xml"):
        str_dump = None
        if file_type == "xml":
            # xml_elem_root = self._build_xml_docs(None, self.dump_to_dict())
            # elem_tree = ET.ElementTree(xml_elem_root)
            # elem_tree.write(file_path, encoding='utf-8')
            # 由于 elem_tree.write() 无法将xml文本进行缩进处理，故更新方法
            str_dump_flat = self.dump_to_xml()
            str_dump = self.make_flat_xml_indent(str_dump_flat)
        elif file_type == "json":
            str_dump = self.dump_to_json()

        with open(file_path, "w") as fp:
            fp.write(str_dump)

    # def make_xml_indent(self):
    #     """ check the text and subnode, set the value of tail or text """
    #     # 由于无法处理 </html> 反缩进量，该功能开发停滞
    #     self.tail = None
    #     self.indent_level = 0

    #     if not self.tag:
    #         raise Exception("Uninitialized Object")

    #     if self.text and self.subnode:
    #         logging.info("Without regard to indent, the text should be NULL while subnode is not NULL.")
    #         return 0
    #     elif self.text and not self.subnode:
    #         logging.debug("A tag like this: <title>Example page</title>")
    #         # 层级不变，保持缩进单位
    #         return 1
    #     elif not self.text and self.subnode:
    #         logging.debug("A tag like this: <html>\n ... \n</html>")
    #         # 层级增加，会影响子层级的tab
    #         self.text = "\n"
    #         return 2
    #     else:  # not self.text and not self.subnode
    #         logging.debug("A tag like this: <page attr='123' />")
    #         # 层级不变，保持缩进单位
    #         return 3

    def _level_xml_line(self, str_line):
        """ 用于判别xml文本中任意一行的类型
            return: True for a new indent;
                    False for an end of indent;
                    None for staying the same indent.
        """
        if re.match(r'</[^><]*>$', str_line):
            logging.debug("匹配 </xxx>")
            return False
        elif re.match(r'<\?[^><]*\?>$', str_line):
            logging.debug("匹配 <? ... ?>")
            return None
        elif re.match(r'<!--', str_line) or re.match(r'-->$', str_line):
            logging.debug("匹配 <!-- ... -->")
            return None
        elif re.match(r'<[^><]*/>$', str_line):
            logging.debug("匹配 <page attr='123' />")
            return None
        elif re.match(r'<[^><]*>$', str_line):
            logging.debug("匹配 <xxx>")
            return True
        elif re.match(r'<.*>$', str_line):
            logging.debug("匹配 <xxx> ttt </xxx>")
            return None
        elif not str_line:
            logging.debug("Null str_line Found.")
        else:
            logging.exception("Unknown Format XML str_line:" + str_line)

    def make_flat_xml_indent(self, str_xml):
        """ return a string of file adding indent. """
        # 此方法完全独立于 xml.etree.ElementTree 模块
        line_with_tab = []
        line_tail = "\n"
        for line in str_xml.split("\n"):
            # logging.debug("Line[{}] : {}".format(line_index, line))
            indent = self._level_xml_line(line)

            if indent is True:
                line_tail += "\t"
            elif indent is False:
                # 修正下一行indent
                line_tail = line_tail[0:-1]
                # 修复当前行indent
                line_with_tab[-1] = line_tail

            logging.debug([line, line_tail])
            line_with_tab.extend([line, line_tail])
        return "".join(line_with_tab)

    def pretty_xml_file(self, file_path):
        with open(file_path, "r+") as fp:
            xml = fp.read()
            new_xml = self.make_flat_xml_indent(xml)

            fp.seek(0)
            # fp.write("")  # if you need to make sure that file is overload.
            fp.write(new_xml)
